<?php
// use ArrayObject;
// use InvalidArgumentException;
// use Exception;
// use View;
// use Router;
// use DateTime;
use Template as View;

/**
 *
 * @author Jakub Dykowski
 *        
 */
class Presenter {
	/**
	 * Error messages, where key corresponds to its POST/GET param name.
	 *
	 * @var array of strings
	 */
	protected $errors;
	/**
	 * Data loaded from post (only those that has been validated, but not necesarily valid).
	 * If some POST/GET param was not validated(validate..() function was not called for him) it won't appear here.
	 *
	 * @var \ArrayObject of data
	 */
	protected $data;
	/**
	 * The access level of current user or null if not logged in.
	 *
	 * @var int
	 */
	protected $level;
	/**
	 * Current user_id or null
	 *
	 * @var int
	 */
	protected $user_id;
	
	/**
	 * Data shared between controllers.
	 *
	 * @var \ArrayObject
	 */
	protected $context;

	/**
	 *
	 * @param int $user_id
	 *        	unique user id or null(...Access, ...Role functions won't work)
	 * @param int $level
	 *        	user access level, 1=admin (always has access), or null(...Level functions won't work)
	 * @throws InvalidArgumentException if $level not integer >= 1 or $user_id is not numeric
	 */
	function __construct($user_id = null, $level = null) {
		if ($level !== null && (!\is_numeric($level) or $level < 1))
			throw new InvalidArgumentException('$level has to be an integer >= 1', $code, $previous);
		if ($user_id !== null)
			$this->user_id = intval($user_id);
		if ($user_id !== null && !\is_numeric($user_id))
			throw new InvalidArgumentException('$level has to be an integer', $code, $previous);
		if ($level !== null)
			$this->level = intval($level);
		$this->data = new ArrayObject(array (), ArrayObject::ARRAY_AS_PROPS);
		$this->errors = new ArrayObject(array (), ArrayObject::ARRAY_AS_PROPS);
		
		// move controller context to static variable, consider even the idea of 1 global shared context between controllers
		$context = $GLOBALS['controller_context'];
		if ($context == null)
			$context = $GLOBALS['controller_context'] = new \ArrayObject(array (), \ArrayObject::ARRAY_AS_PROPS);
		$this->context = $context;
	}

	/**
	 * Checks wheter this request is POST request.
	 *
	 * @return boolean whether it is POST
	 */
	protected function isPost() {
		return $_SERVER['REQUEST_METHOD'] == 'POST';
	}

	/**
	 * Redirects only on this website.
	 * If $url starts with '/' then it will be absolute website path, otherwise it will be url relative to this controller path (without action).
	 *
	 * @param string $url        	
	 */
	protected function redirect(string $url) {
		if (strpos($url, "/") !== 0) {
			$context = Router::getCurrentContext();
			$url = $context->getController() . '/' . $url;
		}
		
		header("Location: " . WEB_PATH . $url != '' ? $url : self::getUrl());
	}

	/**
	 * If $to is NULL returns whole current url.
	 * Allowed $to is: controller, action.
	 *
	 * @param string $to
	 *        	the last element that url should contain
	 * @throws InvalidArgumentException
	 * @return string Ambigous NULL
	 */
	protected function getUrl(string $to = null) {
		$context = Router::getCurrentContext();
		if ($to == null)
			return Router::getGlobalContext()->getUrl();
		
		switch ($to) {
			case 'action':
				return $context->getAction();
			case 'controller':
				return $context->getController();
			default :
				throw new InvalidArgumentException("Unsupported \$what value: $to");
		}
	}

	protected function validateText($param, $required, $min = null, $max = null) {
		if ($result = self::validateRequired($param, $required)) {
			$value = trim($_REQUEST[$param]);
			if ($min !== null and strlen($value) < $min)
				$this->errors[$param] = "min. $min znaki";
			else if ($max !== null and strlen($value) > $max)
				$this->errors[$param] = "maks. $max znaki";
			$result = !$this->errors[$param];
		}
		// $this->data->$param = $_REQUEST[$param];
		return (bool) $result;
	}

	protected function validateInteger($param, $required, $min = null, $max = null) {
		if ($result = self::validateRequired($param, $required)) {
			$value = trim($_REQUEST[$param]);
			if (!preg_match('/^\d+$/', $value))
				$this->errors[$param] = "musi być liczbą";
			else if ($min !== null and $value < $min)
				$this->errors[$param] = "co najmniej $min";
			else if ($max !== null and $value > $max)
				$this->errors[$param] = "co najwyżej $max";
			$result = !$this->errors[$param];
		}
		// $this->data->$param = $_REQUEST[$param];
		return (bool) $result;
	}

	protected function validateDecimal($param, $required, $min = null, $max = null) {
		if ($result = self::validateRequired($param, $required)) {
			$value = trim($_REQUEST[$param]);
			if (!is_numeric($value))
				$this->errors[$param] = "musi być liczbą";
			else if ($min !== null and $value < $min)
				$this->errors[$param] = "co najmniej $min";
			else if ($max !== null and $value > $max)
				$this->errors[$param] = "co najwyżej $max";
			$result = !$this->errors[$param];
		}
		// $this->data->$param = $_REQUEST[$param];
		return (bool) $result;
	}

	protected function validateIn($param, $required, $options) {
		if ($result = self::validateRequired($param, $required)) {
			if (!in_array(trim($_REQUEST[$param]), $options))
				$this->errors[$param] = "wartość spoza zakresu";
			$result = !$this->errors[$param];
		}
		// $this->data->$param = $_REQUEST[$param];
		return (bool) $result;
	}

	protected function validateDate($param, $required, $message = null, $format = 'Y-m-d') {
		if ($result = self::validateRequired($param, $required)) {
			$date = DateTime::createFromFormat($format, trim($_REQUEST[$param]));
			$datetimeErrors = DateTime::getLastErrors();
			if ($datetimeErrors['error_count'] > 0)
				$this->errors[$param] = ($message === null ? "data w niepoprawnym formacie (RRRR-MM-DD)" : $message);
			$result = !$this->errors[$param];
		}
		// $this->data->$param = $_REQUEST[$param];
		$this->data->$param = $date->format('Y-m-d');
		return (bool) $result;
	}

	protected function validateRegex($param, $required, $regex, $message = null) {
		if ($result = self::validateRequired($param, $required)) {
			if (!preg_match("/$regex/", trim($_REQUEST[$param])))
				$this->errors[$param] = $message !== null ? $message : "nieprawidłowa wartość";
			if ($date == false)
				$this->errors[$param] = $message;
			$result = !$this->errors[$param];
		}
		// $this->data->$param = $_REQUEST[$param];
		return (bool) $result;
	}

	protected function validateRequired($param, $required = true) {
		$value = trim($_REQUEST[$param]);
		if ($required and !$value and $value !== '0') {
			$this->errors[$param] = "uzupełnij";
			$this->data->$param = null;
			return false;
		}
		
		$this->data->$param = $value != "" ? $value : null;
		return (bool) $value != "";
	}

	/**
	 * Checks whether all validated data is valid.
	 *
	 * @return boolean
	 */
	protected function isValid() {
		return !(count($this->errors));
	}

	/**
	 * Checks if current user has one of given access levels.
	 * Int '1' is assumed to be admin.
	 * Admin has access, ALWAYS.
	 *
	 * @param int $level,...
	 *        	unlimited number of access levels, one of which user has to have
	 * @throws \AccessDeniedException if access is denied
	 */
	protected function verifyLevel($level) {
		$args = func_get_args();
		if ($args_num = func_num_args() > 1)
			if (is_string($message = $args[$args_num - 1]))
				unset($args[$args_num - 1]);
		if (!\call_user_func_array(array (
				$this,
				"checkLevel" 
		), $args)) {
			throw new \AccessDeniedException($message);
		}
	}

	/**
	 * Checks whether current user level is equal to one of the specified levels.
	 * Each level need to be an int (not string).
	 *
	 * @param int $level,...
	 *        	unlimited number of access levels
	 * @return boolean whether access is granted
	 */
	protected function checkLevel($level) {
		if ($this->level === null)
			throw new RuntimeException("this controller doesn't have level set");
		return $this->level === 1 /*ADMIN*/ ? true : in_array($this->level, func_get_args(), true);
	}

	protected function verifyRole() {
		if ($this->user_id === null)
			throw new RuntimeException("this controller doesn't have user_id set");
			// TODO implement role access
		throw new \Exception('not implemented, need acl service');
	}

	protected function checkRole() {
		if ($this->user_id === null)
			throw new RuntimeException("this controller doesn't have user_id set");
			// TODO implement role access
		throw new \Exception('not implemented, need acl service');
	}

	/**
	 * Coveniently creates and sets the view data, including: $this->data as $data, $this->errors as $errors, getUrl() as $url.
	 * Errors as set only when it is POST and there are any.
	 *
	 * @param string $path
	 *        	the view path
	 * @return View
	 */
	protected function template($path) {
		$view = new View($path);
		$view->data = $this->data;
		if (!self::isValid())
			$view->errors = $this->errors;
		$view->url = self::getUrl();
		$view->level = $this->level;
		$view->context = $this->context;
		return $view;
	}
}